package icasue.net.protocol.http;

import icasue.net.base.annotations.NotNull;
import icasue.net.base.annotations.Nullable;
import icasue.net.protocol.NetProtocol;
import icasue.net.protocol.http.common.Const;
import icasue.net.protocol.http.enums.MediaType;

import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.time.LocalDateTime;
import java.util.*;

import static icasue.net.protocol.http.common.Const.*;

/**
 * @Author: Qiao Hang
 * @CreateDate: 2021/3/11 上午11:12
 * @UpdateDate:
 * @Description:
 */
public class HttpProtocol implements NetProtocol {

    private static final String CONTENT_TYPE = "Content-Type";
    private static final String CONTENT_LENGTH = "Content-Length";
    private static final String formQueryFormat = "Content-Disposition: form-data; name=\"%s\"";
    private static final String formFileFormat = "Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"";
    private static final String formFileContentType = "Content-Type: text/plain";
    private static final String fileNameLenMatchError = "Create Http Protocol error, multipart/form-data's file stream file's name don't match!";
    private static final String fileApiParamLenMatchError = "Create Http Protocol error, multipart/form-data's file stream size and API's file param don't match!";
    private static final String $UTF_8 = "UTF-8";
    private static final Charset UTF_8 = Charset.forName($UTF_8);

    private StringBuilder URI;
    private HttpOption httpOption;
    private String protocolHead;
    private Map<String,String> headers;
    private Map<String,Object> query;

    private String body;

    private Map<String,Object> form;
    private Map<String,Object> xxxFormUrlEncoded;
    private List<String> fNames;
    private List<String> fPNames;
    private List<byte[]> bytes;
    private volatile String boundary;

    {
        headers = new HashMap<String, String>(){{
            /*默认Header HOST 为127.0.0.1*/
            put("Host","127.0.0.1");
            put("Accept","*/*");
            put("Accept-Encoding","gzip, deflate, br");
            put("Connection","close");
        }};
    }

    protected HttpProtocol(@NotNull StringBuilder URI, @NotNull HttpOption httpOption){
        if(!URI.substring(0,1).equals($PATH_DELIMITER)){
            URI = URI.reverse().append($PATH_DELIMITER).reverse();
        }
        this.URI = URI;
        this.httpOption = httpOption;
    }

    public static HttpProtocol builder(@NotNull HttpOption option, @NotNull String URI){
        URI = URI.trim();
        int existQuery = URI.indexOf($QUERY);
        String simpleURI = existQuery > 0 ? URI.substring(0,existQuery) : URI;
        try {
            new URI(simpleURI);
        }catch (URISyntaxException e){/*抛URI格式异常*/}
        HttpProtocol protocol = new HttpProtocol(new StringBuilder(simpleURI), option);
        if(existQuery > 0){
            String[] directQueries = URI.substring(existQuery + 1).split($AND);
            for (String directQuery : directQueries) {
                int queryItemDeli = directQuery.indexOf(EQ);
                if(queryItemDeli > 0)
                    protocol.query(directQuery.substring(0,queryItemDeli),directQuery.substring(queryItemDeli + 1));
            }
        }
        return protocol;
    }

    public HttpProtocol headers(Map<String, String> headers){
        if(headers != null){
            this.headers.putAll(headers);
        }
        return this;
    }

    public HttpProtocol header(String header,String val){
        if(header != null && val != null){
            this.headers.put(header,val);
        }
        return this;
    }

    public HttpProtocol queries(Map<String,Object> query){
        if(query != null && query.size() > 0){
            Map<String,Object> copy = new HashMap<>(query);
            for (Map.Entry<String, Object> kv : query.entrySet()) {
                if(kv.getValue() == null)
                    copy.remove(kv.getKey());
            }
            if(!copy.isEmpty()){
                this.query = Optional.ofNullable(this.query).orElseGet(() -> new HashMap<>());
                this.query.putAll(copy);
            }
        }
        return this;
    }

    public HttpProtocol query(String key,Object val){
        if(key != null && val != null){
            this.query = Optional.ofNullable(this.query).orElseGet(() -> new HashMap<>());
            this.query.put(key,val);
        }
        return this;
    }

    public HttpProtocol body(String json){
        this.body = json;
        return this;
    }

    public HttpProtocol forms(Map<String,Object> formParams){
        if(formParams != null){
            Map<String, Object> copy = new HashMap<>(formParams);
            for (Map.Entry<String, Object> kv : formParams.entrySet()) {
                if(kv.getValue() == null)
                    copy.remove(kv.getKey());
            }
            if(!copy.isEmpty()){
                createBoundary();
                this.form = copy;
            }
        }
        return this;
    }

    public HttpProtocol form(String key,Object val){
        if(key != null && val != null){
            createBoundary();
            if(this.form == null)
                this.form = new HashMap<>();
            this.form.put(key,val);
        }
        return this;
    }

    public HttpProtocol wwwFormsUrlEncoded(Map<String, Object> formsParams){
        if(formsParams != null){
            Map<String, Object> copy = new HashMap<>(formsParams);
            for (Map.Entry<String, Object> kv : formsParams.entrySet()) {
                if(kv.getValue() == null)
                    copy.remove(kv.getKey());
            }
            if(!copy.isEmpty()){
                this.xxxFormUrlEncoded = copy;
            }
        }
        return this;
    }

    public HttpProtocol wwwFormUrlEncoded(String key, Object val){
        if(key != null && val != null){
            if(this.xxxFormUrlEncoded == null)
                this.xxxFormUrlEncoded = new HashMap<>();
            this.xxxFormUrlEncoded.put(key,val);
        }
        return this;
    }

    /**
     * 多文件流读取.
     * @param fields_Files
     * @return
     */
    public HttpProtocol upFileMulti(Map<String,File> fields_Files){
        if(fields_Files != null && fields_Files.size() > 0){
            for (Map.Entry<String, File> field_File : fields_Files.entrySet()) {
                File file = field_File.getValue();
                String field = field_File.getKey();
                if(file == null || !file.exists()){ continue; }
                checkInitXStream();
                FileInputStream fileIn = null;
                try {
                    fileIn = new FileInputStream(file.getAbsolutePath());
                    byte[] streamBytes = streamRead(fileIn);
                    bytes.add(streamBytes);
                    fNames.add(file.getName());
                    fPNames.add(field);
                }catch (FileNotFoundException notFound){
                    /*这里文件基本一定存在，除非有另外的线程正在删除该文件.*/
                }catch (IOException ioError){
                    /*抛IO运行时异常*/
                    throw new RuntimeException("Occur IOException when builder Http POST Protocol!");
                }finally {
                    if(fileIn != null){
                        try {
                            fileIn.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        return this;
    }

    /**
     * 文件流读取.
     * @param file
     * @param fieldName
     * @return
     */
    public HttpProtocol upFile(File file,String fieldName){
        if(file != null && fieldName != null && file.exists()){
            checkInitXStream();
            FileInputStream fileIn = null;
            try {
                fileIn = new FileInputStream(file.getAbsolutePath());
                byte[] streamBytes = streamRead(fileIn);
                bytes.add(streamBytes);
                fNames.add(file.getName());
                fPNames.add(fieldName);
            }catch (FileNotFoundException notFound){
                /*这里文件基本一定存在，除非有另外的线程正在删除该文件.*/
            }catch (IOException ioError){
                /*抛IO运行时异常*/
                throw new RuntimeException("Occur IOException when builder Http POST Protocol!");
            }finally {
                if(fileIn != null){
                    try {
                        fileIn.close();
                    }catch (Throwable e){
                        e.printStackTrace();
                    }
                }
            }
        }
        return this;
    }


    /**
     * Multiply File Bytes
     * @param field_bytes
     * @param fileNames
     * @return
     */
    public HttpProtocol bytesMulti(Map<String,byte[]> field_bytes,List<String> fileNames){
        if(field_bytes != null && field_bytes.size() > 0 && fileNames != null){
            /*抛长度不匹配异常/文件名缺失异常*/
            if(field_bytes.size() != fileNames.size()){
                throw new RuntimeException(fileNameLenMatchError);
            }
            int idx = 0;
            for (Map.Entry<String, byte[]> field_byteAry : field_bytes.entrySet()) {
                String field = field_byteAry.getKey();
                byte[] fileBytes = field_byteAry.getValue();
                String fileName = fileNames.get(idx++);
                if(fileBytes != null && fileName != null){
                    checkInitXStream();
                    bytes.add(fileBytes);
                    fNames.add(field);
                    fPNames.add(fileName);
                }
            }
        }
        return this;
    }

    /**
     * 文件bytes
     * @param fileBytes
     * @param fileName
     * @param fieldName
     * @return
     */
    public HttpProtocol bytes(String fieldName,byte[] fileBytes,String fileName){
        if(fieldName != null && fileBytes != null && fileName != null){
            checkInitXStream();
            bytes.add(fileBytes);
            fNames.add(fileName);
            fPNames.add(fieldName);
        }
        return this;
    }


    /**
     * 多文件流
     * @param fields_streams
     * @param fileNames
     * @return
     */
    public HttpProtocol xStreamMulti(Map<String,InputStream> fields_streams,List<String> fileNames){
        if(fields_streams != null && fields_streams.size() > 0 && fileNames != null){
            /*抛长度不匹配异常/文件名缺失异常*/
            if(fields_streams.size() != fileNames.size()){
                throw new RuntimeException(fileNameLenMatchError);
            }
            int idx = 0;
            for (Map.Entry<String, InputStream> field_steam : fields_streams.entrySet()) {
                String field = field_steam.getKey();
                InputStream stream = field_steam.getValue();
                String fileName = fileNames.get(idx++);
                if(stream != null && fileName != null){
                    checkInitXStream();
                    try {
                        byte[] streamBytes = streamRead(stream);
                        bytes.add(streamBytes);
                        fNames.add(field);
                        fPNames.add(fileNames.get(idx++));
                    }catch (IOException e){
                        /*抛IO运行时异常*/
                        throw new RuntimeException("Occur IOException when builder Http POST Protocol!");
                    }
                }
            }
        }
        return this;
    }

    /**
     * 单文件流
     * @param fieldName
     * @param xStream
     * @param fileName
     * @return
     */
    public HttpProtocol xStream(String fieldName,InputStream xStream,String fileName) {
        if (fieldName != null && xStream != null && fileName != null) {
            checkInitXStream();
            try {
                byte[] streamBytes = streamRead(xStream);
                bytes.add(streamBytes);
                fNames.add(fileName);
                fPNames.add(fieldName);
            } catch (IOException e) {
                /*抛IO运行时异常*/
                throw new RuntimeException("Occur IOException when builder Http POST Protocol!");
            }
        }
        return this;
    }

    /**
     * Read inStream to byte[].
     * @param inStream
     * @return
     * @throws IOException
     */
    private byte[] streamRead(@NotNull InputStream inStream) throws IOException {
        ByteArrayOutputStream byteArrayStream = null;
        try {
            int b = -1;
            int idx = 0;
            byteArrayStream = new ByteArrayOutputStream();
            while ((b = inStream.read()) != -1){
                byteArrayStream.write(b);
                if(++idx == 1024){
                    byteArrayStream.flush();
                    idx = 0;
                }
            }
            if(idx != 0){
                byteArrayStream.flush();
            }
            byte[] bytes = byteArrayStream.toByteArray();
            return bytes;
        }catch (IOException e){
            throw e;
        }finally {
            if(byteArrayStream != null){
                byteArrayStream.close();
            }
        }
    }

    /**
     * Check Init xStream's buffer.
     */
    private void checkInitXStream(){
        if(fNames == null){
            fNames = new ArrayList<>();
        }else {
            return;
        }
        if(bytes == null){
            bytes = new ArrayList<>();
        }
        if(fPNames == null){
            fPNames = new ArrayList<>();
        }
        createBoundary();
    }

    /**
     * Init form-data's boundary.
     */
    private void createBoundary(){
        if(this.boundary == null){
            this.boundary = String.join($STRING_EMPTY,
                    $DECR_SYMBOL,$DECR_SYMBOL,$DECR_SYMBOL,$DECR_SYMBOL,$DECR_SYMBOL,
                    DF_COMBINE.format(LocalDateTime.now()));
        }
    }

    private void preBuild(@Nullable String remoteHost) {
        if(remoteHost != null && !remoteHost.isEmpty()){
            if(!remoteHost.equals("localhost")){
                this.headers.put("Host", remoteHost);
            }
        }else {
            /*Using local address 127.0.0.1 as net address.*/
        }
    }

    /**
     * Http protocol's build chooser.
     * @return
     */
    public StringBuilder build(@Nullable String remoteHost){
        /*PreBuild do something prepare.*/
        preBuild(remoteHost);
        /*Build protocol.*/
        StringBuilder protocol = new StringBuilder();
        int version = httpOption.getVersion();
        if(version == HttpOption.V1_1.GET.version || version == HttpOption.V2_0.GET.version){
            // GET
            buildGet(protocol);
        }else if(version == HttpOption.V1_1.POST.version || version == HttpOption.V2_0.POST.version){
            // POST
            buildPost(protocol);
        }else {
            // PUT DELETE OPTION WILL BE EXPLAINED AFTER.
        }
        return protocol;
    }

    private String urlEncoding(Map<String,Object> urlParams){
        StringBuilder builder = new StringBuilder();
        if(urlParams != null && !urlParams.isEmpty()){
            for (Map.Entry<String, Object> pv : urlParams.entrySet()) {
                String valEncode = String.valueOf(pv.getValue());
                try {
                    valEncode = URLEncoder.encode(valEncode, $UTF_8);
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                    valEncode = String.valueOf(pv.getValue());
                }
                builder.append(pv.getKey()).append(EQ).append(valEncode).append($AND);
            }
            builder.replace(builder.length() - 1,builder.length(), $STRING_EMPTY);
        }
        return builder.toString();
    }

    private void buildProtocolHead(){
        //FULL URI.
        // ? -> %3F, & -> &, = -> %3D
        if(query != null && query.size() > 0){
            if(URI.indexOf($QUERY) < 0){
                URI.append($QUERY);
            }
            String urParamsEncoding = urlEncoding(query);
            URI.append(urParamsEncoding);
        }
        protocolHead = String.format(httpOption.getHeaderFormat(),URI.toString());
    }

    private void buildGet(StringBuilder buffer) {
        //build protocol head.
        buildProtocolHead();
        //protocol head
        buffer.append(protocolHead).append(PROTOCOL_HTTP_LINE);

        //forms get.
        StringBuilder contentTmp = null;
        if(this.form != null && !this.form.isEmpty()){
            contentTmp = new StringBuilder();
            for (Map.Entry<String, Object> kv : form.entrySet()) {
                contentTmp.append(PROTOCOL_HTTP_LINE).append($DECR_SYMBOL).append($DECR_SYMBOL).append(boundary).append(PROTOCOL_HTTP_LINE)
                        .append(String.format(formQueryFormat,kv.getKey())).append(PROTOCOL_HTTP_LINE)
                        .append(PROTOCOL_HTTP_LINE)
                        .append(kv.getValue());
            }
            //end content.
            contentTmp.append(PROTOCOL_HTTP_LINE)
                    .append($DECR_SYMBOL).append($DECR_SYMBOL)
                    .append(boundary)
                    .append($DECR_SYMBOL).append($DECR_SYMBOL)
                    .append(PROTOCOL_HTTP_LINE);
            //Content-Type. Content-Length.
            headers.put(CONTENT_TYPE,"multipart/form-data; boundary=" + boundary);
            headers.put(CONTENT_LENGTH,String.valueOf(contentTmp.toString().getBytes(UTF_8).length - 2));
        }

        //xxxFormUrlEncoded get
        if(this.xxxFormUrlEncoded != null && !this.xxxFormUrlEncoded.isEmpty()){
            contentTmp = new StringBuilder();
            //start content.
            contentTmp.append(PROTOCOL_HTTP_LINE);
            // content.
            String urlParamsEncoding = urlEncoding(xxxFormUrlEncoded);
            contentTmp.append(urlParamsEncoding);

            //Content-Type. Content-Length.
            headers.put(CONTENT_TYPE,MediaType.APPLICATION_FORM_URLENCODED_VALUE);
            headers.put(CONTENT_LENGTH,String.valueOf(contentTmp.toString().getBytes(UTF_8).length - 2));
        }

        //protocol headers
        headers.entrySet().forEach((kv) -> buffer.append(kv.getKey()).append($COLON).append(SPACE).append(kv.getValue()).append(PROTOCOL_HTTP_LINE));

        //append protocol end only if no form args included in this request.
        if(contentTmp == null){
            //queries's simple protocol end.
            buffer.append(PROTOCOL_HTTP_LINE);
        }else {
            buffer.append(contentTmp);
        }
    }


    private void buildPost(StringBuilder buffer) {
        // protocol content, notice content include header's delimiter "\r\n", Content-Length need to reduce.
        StringBuilder contentTmp = new StringBuilder();
        //build protocol head.
        buildProtocolHead();
        //protocol head
        buffer.append(protocolHead).append(PROTOCOL_HTTP_LINE);

        // Content-Type: application/json
        if(body != null && body.length() > 0){
            // application/json's request.
            headers.put(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
            contentTmp.append(PROTOCOL_HTTP_LINE).append(body);
        }
        // Content-Type: multipart/form-data; boundary=***
        else if((form != null && form.size() > 0) || (bytes != null && bytes.size() > 0)){
            headers.put(CONTENT_TYPE,"multipart/form-data; boundary=" + boundary);
            // include form-data's text request.
            if(form != null && form.size() > 0){
                for (Map.Entry<String, Object> kv : form.entrySet()) {
                    contentTmp.append(PROTOCOL_HTTP_LINE).append($DECR_SYMBOL).append($DECR_SYMBOL).append(boundary).append(PROTOCOL_HTTP_LINE)
                            .append(String.format(formQueryFormat,kv.getKey())).append(PROTOCOL_HTTP_LINE)
                            .append(PROTOCOL_HTTP_LINE)
                            .append(kv.getValue());
                }
            }
            // include form-data's file request.
            if(bytes != null && bytes.size() > 0){
                int idx = 0;
                for (byte[] fileBytes : bytes) {
                    contentTmp.append(PROTOCOL_HTTP_LINE).append($DECR_SYMBOL).append($DECR_SYMBOL).append(boundary).append(PROTOCOL_HTTP_LINE)
                            .append(String.format(formFileFormat,fPNames.get(idx),fNames.get(idx))).append(PROTOCOL_HTTP_LINE)
                            .append(formFileContentType).append(PROTOCOL_HTTP_LINE)
                            .append(PROTOCOL_HTTP_LINE)
                            .append(new String(fileBytes,UTF_8));
                    idx++;
                }
            }
            //end content, by this case POST option has form-data part.
            contentTmp.append(PROTOCOL_HTTP_LINE)
                    .append($DECR_SYMBOL).append($DECR_SYMBOL)
                    .append(boundary)
                    .append($DECR_SYMBOL).append($DECR_SYMBOL)
                    .append(PROTOCOL_HTTP_LINE);
        }
        // Content-Type: application/x-www-form-urlencoded
        else if(xxxFormUrlEncoded != null && !xxxFormUrlEncoded.isEmpty()){
            headers.put(CONTENT_TYPE,MediaType.APPLICATION_FORM_URLENCODED_VALUE);
            String formUrlParamsEncoded = urlEncoding(xxxFormUrlEncoded);
            contentTmp.append(PROTOCOL_HTTP_LINE).append(formUrlParamsEncoded);
        }
        // Has no Content-Type, Content-Length is zero.
        else {
            //end content, by this case POST option has not form-data part, perhaps exist queries only.
            headers.put(CONTENT_LENGTH, Const.BLOCK_SIZE_ZERO);
            contentTmp.append(PROTOCOL_HTTP_LINE);
        }
        //protocol's Header Content-Length, notice here need to reduce head's end : "\r\n" 's byte length.
        headers.put(CONTENT_LENGTH,String.valueOf(contentTmp.length()));
        if(contentTmp.length() > 0){
            headers.put(CONTENT_LENGTH,String.valueOf(contentTmp.toString().getBytes(UTF_8).length - 2));
        }
        //protocol headers
        headers.entrySet().forEach((kv) -> buffer.append(kv.getKey()).append($COLON).append(SPACE).append(kv.getValue()).append(PROTOCOL_HTTP_LINE));
        //protocol content
        buffer.append(contentTmp);
    }


}
